import java.awt.Color;
import java.awt.Graphics;
import java.awt.Point;
import java.util.ArrayList;
import java.util.Iterator;

public class Neuron extends DrawObject
{
    // DrawObject abilities:
    @Override
    public boolean IsNeuron()
    {
        return true;
    }

    @Override
    public boolean CanBeDeletedByUser()
    {
        return true;
    }

    @Override
    public boolean CanBeExcitedByUser()
    {
        return true; // TODO: is this true?
    }

    @Override
    public boolean CanBeMovedByUser()
    {
        return true;
    }

    @Override
    public boolean CanBeSelectedByUser()
    {
        return true;
    }
    // end of DrawObject abilities

    private ArrayList<NeuronInputField> InputFields = new ArrayList<NeuronInputField>(); // always add field before node!
    private ArrayList<NeuronInputNode> InputNodes = new ArrayList<NeuronInputNode>();
    private ArrayList<NeuronOutputNode> OutputNodes = new ArrayList<NeuronOutputNode>();

    private int TickCountSinceLastActivation = 0;

    protected class NeuronInputDrawObjects
    {
        private NeuronInputField Field;
        private NeuronInputNode Node;

        protected NeuronInputDrawObjects(NeuronInputField Field, NeuronInputNode Node)
        {
            this.Field = Field;
            this.Node = Node;
        }

        public NeuronInputField GetField()
        {
            return Field;
        }

        public NeuronInputNode GetNode()
        {
            return Node;
        }
    };

    public Neuron(Point Pos)
    {
        SetPos(Pos);
        SetSize(new Point(100, 100));
        InitializeExcitement(ExcitementOnceMilliseconds);
        SetCompoundId(GetNewCompoundId());
    };

    public static String ExtractHormoneName(String Line)
    {
        // example: "Adrenaline=50", or "Aggression=-30", or "Hormone.Sex=+30", just "x=y[percent]"

        try
        {
            String HormoneName = "";
            int Pos = Line.indexOf("=");
            if (Pos >= 0)
            {
                HormoneName = Line.substring(0, Pos);
                return HormoneName;
            }
            else
            {
                return "";
            }
        }
        catch (Exception e) // for compatibility with ExtractHormoneStrength()
        {
            return "";
        }
    };

    public static double ExtractHormoneStrength(String Line)
    {
        // example: "Adrenaline=50", or "Aggression=-30", or "Hormone.Sex=+30", just "x=y[percent]"

        try
        {
            double HormoneStrength = 0.0;
            int Pos = Line.indexOf("=");
            if (Pos >= 0)
            {
                HormoneStrength = ((double) Integer.parseInt(Line.substring(Pos + 1))) / 100.0;
                return HormoneStrength;
            }
            else
            {
                return 0.0; // default (means Hormone inactive)
            }
        }
        catch (Exception e) // we expect NumberFormatException, if any
        {
            return 0.0; // default (means Hormone inactive)
        }
    };

    public static double LimitHormoneStrength(double Strength)
    {
        if (Strength < -1.0)
            Strength = -1.0;
        if (Strength > +1.0)
            Strength = +1.0;

        return Strength;
    }

    @Override
    protected void AddExcitementForNextTick(double ExcitementAdd)
    {
        // Neuron: NeuronInputFields DIRECTLY call this.ExciteAsPartner() instead!
    }

    public void AddHormone(String HormoneName)
    {
        if (HormoneName != null && HormoneName.length() >= 1) // user can delete any Hormone from any Neuron by just setting the Neuron's Hormone name to "", after saving to and re-loading from file the Neuron does not contain the Hormone any more
        {
            if (HormoneNames == EmptyHormoneNames)
                HormoneNames = new ArrayList<String>();
            if (HormoneStrengths == EmptyHormoneStrengths)
                HormoneStrengths = new ArrayList<Double>();

            HormoneNames.add(HormoneName);
            HormoneStrengths.add(0.0);

            AutoSize(); // make space for HormoneName=HormoneStrength string printed at bottom
        }
        else
        {
            AutoSize(); // do in any case to resize after having reloaded Neuron from file
        }
    }

    public NeuronInputDrawObjects AddInputFieldAndNode()
    { // spawns child objects

        Point NeuronInputFieldPos = new Point(Pos.x, Pos.y + InputFields.size() * 20);
        Point NeuronInputNodePos = new Point(Pos.x - 10, Pos.y + InputFields.size() * 20);

        NeuronInputField NeuronInputFieldNew = new NeuronInputField(NeuronInputFieldPos);
        NeuronInputNode NeuronInputNodeNew = new NeuronInputNode(NeuronInputNodePos);

        NeuronInputNodeNew.SetCompoundId(((Neuron) this).CompoundId);
        NeuronInputNodeNew.SetParentDrawObject(this);
        NeuronInputNodeNew.ExcitementPartner = NeuronInputFieldNew;
        NeuronInputFieldNew.SetCompoundId(((Neuron) this).CompoundId);
        NeuronInputFieldNew.SetParentDrawObject(this);
        NeuronInputFieldNew.ExcitementPartner = (Neuron) this;

        InputFields.add(NeuronInputFieldNew);
        InputNodes.add(NeuronInputNodeNew);

        AutoSize();

        return new NeuronInputDrawObjects(NeuronInputFieldNew, NeuronInputNodeNew);
    }

    public void AddInputFieldAndNode(NeuronInputField AddField, NeuronInputNode AddNode)
    { // add child objects directly (during file reload)

        AddNode.SetParentDrawObject(this);
        AddField.SetParentDrawObject(this);

        AddNode.ExcitementPartner = AddField;
        AddField.ExcitementPartner = ((Neuron) this);

        InputFields.add(AddField);
        InputNodes.add(AddNode);

        AutoSize();
    }

    public NeuronOutputNode AddOutputNode()
    {
        NeuronOutputNode NeuronOutputNodeNew = new NeuronOutputNode(new Point(Pos.x + 100, Pos.y + Size.y / 2));

        NeuronOutputNodeNew.SetCompoundId(((Neuron) this).CompoundId);
        NeuronOutputNodeNew.SetParentDrawObject(this);

        ((Neuron) this).ExcitementPartner = NeuronOutputNodeNew;

        OutputNodes.add(NeuronOutputNodeNew);

        AutoSize();

        return NeuronOutputNodeNew;
    }

    public void AddOutputNode(NeuronOutputNode AddNode)
    {
        AddNode.SetParentDrawObject(this);

        ((Neuron) this).ExcitementPartner = AddNode;

        OutputNodes.add(AddNode);

        AutoSize();
    }

    @Override
    protected void AfterCollectingAllExcitementForNextTick()
    {
        if (IsInLearningNeuronState)
        {
            boolean NegativeInputFieldActivated = false;

            // If this is a LearningNeuron, the Strength of those NeuronInputFields
            // is increased by one which were excited in case that ADDITIONALLY one
            // negative NeuronInputField (the user can toggle with "n" key press)
            // was excited, too. If no negative NeuronInputField was excited, the
            // LearningNeuron behaves just like a normal Neuron, it just activates
            // its NeuronOutputNode if the internal excitement exceeds the trigger
            // value.

            for (Iterator<NeuronInputField> i = InputFields.iterator(); i.hasNext();)
            {
                NeuronInputField o = i.next();
                if (o.IsNegative && o.ExcitementPartnerServedInThisTick)
                {
                    NegativeInputFieldActivated = true;
                    break;
                }
            }
            if (NegativeInputFieldActivated) // mandatory condition to increase any NeuronInputField.Strength value
            {
                int ServerServedCount = 0;
                @SuppressWarnings("unused")
                int ServerMissedCount = 0;

                for (Iterator<NeuronInputField> i = InputFields.iterator(); i.hasNext();)
                {
                    NeuronInputField o = i.next();
                    if (!(o.IsNegative))
                    {
                        if (o.ExcitementPartnerServedInThisTick)
                            ServerServedCount++;
                        else
                            ServerMissedCount++;
                    }
                }
                if (ServerServedCount >= 1) // && ServerMissedCount == 0) // ALL required inputs excited?
                { // currently: just increase Strength of any non-negative field which donated excitement

                    for (Iterator<NeuronInputField> i = InputFields.iterator(); i.hasNext();)
                    {
                        NeuronInputField o = i.next();
                        if (!(o.IsNegative) && o.ExcitementPartnerServedInThisTick)
                        {
                            // o.IncreaseStrength();
                            o.SetStrength(o.GetStrengthMax());
                        }
                    }
                }
            }
        }
    }

    private void AutoSize()
    {
        if ((InputFields.size() + HormoneNames.size()) > 100 / 20)
        {
            Size.y = InputFields.size() * 20 + HormoneNames.size() * 20;
        }
        else
        {
            Size.y = 100;
        }
    }

    public void DeleteHormone(String HormoneName)
    {
        for (int i = 0; i < HormoneNames.size(); i++)
        {
            if (HormoneNames.get(i).equals(HormoneName))
            {
                HormoneNames.remove(i);
                HormoneStrengths.remove(i);
                i--;
            }
        }

        AutoSize();
    }

    @Override
    public void Draw(Graphics g)
    {
        if (TickCountSinceLastActivation < Integer.MAX_VALUE) // for drawing only (keep longer in "activated" color as a visual help for user)
            TickCountSinceLastActivation++;

        if (!(IsWithinWindowBounds(this, 0)))
            return;

        // set font once for speed reasons
        g.setFont(GetDefaultFont());

        // background (overdraw grid)
        if (IsActivated())
            TickCountSinceLastActivation = 0;

        if (IsHighlighted || IsPartOfMultiSelection)
            SetColor(g, Color.GREEN);
        else if (TickCountSinceLastActivation < Simulation.TicksPerIOTextAreaLine())
            SetColor(g, GetExcitementColor(Color.LIGHT_GRAY));
        else
            SetColor(g, Color.LIGHT_GRAY);
        FillRect(g, Pos.x, Pos.y, Size.x, Size.y);

        // child objects
        int InputFieldWidth = -1;
        for (Iterator<NeuronInputField> i = InputFields.iterator(); i.hasNext();)
        {
            NeuronInputField o = i.next();
            // o.Draw(g); // Beta 3: bug fixed: no need for manual Draw() call, system calls Draw() for every DrawObject anyway!
            InputFieldWidth = o.Size.x;
        }
        /* // Beta 3: bug fixed: no need for manual Draw() call, system calls Draw() for every DrawObject anyway!
        for (Iterator<NeuronInputNode> i = InputNodes.iterator(); i.hasNext();)
        {
        	NeuronInputNode o = i.next();
        	o.Draw(g);
        }
        */
        // Hormone info drawn at end

        // excitement bar
        double ExcitementMax = GetExcitementMaxFromExcitementHistory();

        int ExcitementBarWidth = 10;
        int ExcitementBarHeight = (int) (ExcitementMax * Size.y); // WITHIN frame (don't add Pos.y)

        // actual bar
        if (DoesActivate(ExcitementMax))
            SetColor(g, Color.GREEN);
        else
            SetColor(g, Color.DARK_GRAY);
        FillRect(g, Pos.x + Size.x - ExcitementBarWidth, Pos.y + Size.y - ExcitementBarHeight, ExcitementBarWidth, ExcitementBarHeight);

        // bar background (do not highlight, else bar not visible)
        SetColor(g, Color.LIGHT_GRAY);
        FillRect(g, Pos.x + Size.x - ExcitementBarWidth, Pos.y, ExcitementBarWidth, Size.y - ExcitementBarHeight);

        // excitement bar trigger level marking
        int ExcitementBarTriggerLevelMarkingY = Size.y - (int) (ExcitementTriggerValue * Size.y);
        SetColor(g, Color.RED);
        FillRect(g, Pos.x + Size.x - ExcitementBarWidth, Pos.y + ExcitementBarTriggerLevelMarkingY, ExcitementBarWidth, 1);

        // frame
        SetColor(g, GetExcitementColor(Color.BLACK));
        DrawRect(g, Pos.x, Pos.y, Size.x, Size.y);

        if (IsInLearningNeuronState)
        {
            SetColor(g, GetExcitementColor(Color.GRAY));
            DrawRect(g, Pos.x - 1, Pos.y - 1, Size.x + 2, Size.y + 2);
            DrawRect(g, Pos.x - 2, Pos.y - 2, Size.x + 4, Size.y + 4);
            DrawRect(g, Pos.x - 3, Pos.y - 3, Size.x + 6, Size.y + 6);
        }

        // level markings
        SetColor(g, Color.BLACK);
        if (InputFieldWidth > 0)
        {
            DrawString(g, "|0", Pos.x - 4, Pos.y - 6);
            DrawString(g, "|1", Pos.x + InputFieldWidth - 4, Pos.y - 6);
        }
        DrawString(g, "-1", Pos.x + Size.x, Pos.y + 4);
        DrawString(g, "-0", Pos.x + Size.x, Pos.y + Size.y + 4);

        /* // not useful (no real information for user), needs CPU time
        int Stepy = Size.y / StrengthMax;
        for (int m = 0; m < StrengthMax; m++)
        {
        	DrawLine(g, Pos.x + Size.x - 1 - 4, Pos.y + m * Stepy, Pos.x + Size.x - 1, Pos.y + m * Stepy);
        }
        */

        // draw Hormone info
        if (ZoomManager.GetZoomFactor() >= 1.0) // we assume that otherwise DrawString() doesn't draw anything, too
        {
            for (int m = 0; m < HormoneNames.size(); m++)
            {
                String HormoneInfoString = HormoneNames.get(m) + "=" + ((int) (HormoneStrengths.get(m) * 100.0)) + "%";
                int HormoneInfoStringDrawWidth = 3 + HormoneInfoString.length() * 12; // we use the font "Courier New", which is a monospace font - all letters have the same width

                int DrawXPos = Pos.x + 4;
                int DrawYPos = Pos.y + Size.y - (HormoneNames.size() - 1) * 20 + m * 20 - 4;

                if (IsHighlighted || IsPartOfMultiSelection)
                    SetColor(g, Color.GREEN);
                else
                    SetColor(g, Color.LIGHT_GRAY);
                FillRect(g, DrawXPos - 3, DrawYPos - 16, HormoneInfoStringDrawWidth, 20);

                SetColor(g, Color.BLUE);
                DrawString(g, HormoneInfoString, DrawXPos, DrawYPos);
            }
        }
    }

    @Override
    protected void DrawFinal(Graphics g)
    {
        g.setFont(GetHugeFont());
        SetColor(g, GetTransparentColorBlack());
        DrawString(g, "&", Pos.x + Size.x / 2 - 28, Pos.y + Size.y / 2 + 28);
    }

    protected void ExciteAsPartner(double ExcitementAdd, DrawObject Sender)
    {
        double NormalizationFactor = 1.0; // we DO NOT use normalization any more, for two reasons: 1) unrealistic (does the real Neuron know how many synapses it has???), 2) too confusing, just summing up bar values is much easier to understand
        // discarded: 1.0 / (double) InputFields.size() / (double) ExcitementOnceCorrectionFactor;

        double HormoneFactor = 0.0;

        for (int m = 0; m < HormoneStrengths.size(); m++)
            HormoneFactor = Neuron.LimitHormoneStrength(HormoneFactor + HormoneStrengths.get(m));

        if (IsInLearningNeuronState && Sender.IsNegative)
        {
            // ignore excitement, negative NeuronInputField is just a "switch" to allow increasing any NeuronInputField.Strength value(s)
        }
        else
        {
            ExcitementCurrent += (ExcitementAdd * NormalizationFactor * (1.0 + HormoneFactor));
        }
    }

    @Override
    public ArrayList<DrawObject> GetChildDrawObjects()
    {
        ArrayList<DrawObject> ChildDrawObjects = new ArrayList<DrawObject>();

        for (Iterator<NeuronInputField> i = InputFields.iterator(); i.hasNext();)
        {
            NeuronInputField o = i.next();
            ChildDrawObjects.add(o);
        }
        for (Iterator<NeuronInputNode> i = InputNodes.iterator(); i.hasNext();)
        {
            NeuronInputNode o = i.next();
            ChildDrawObjects.add(o);
        }
        for (Iterator<NeuronOutputNode> i = OutputNodes.iterator(); i.hasNext();)
        {
            NeuronOutputNode o = i.next();
            ChildDrawObjects.add(o);
        }

        return ChildDrawObjects;
    }

    public double GetHormoneStrength(String HormoneName)
    {
        for (int m = 0; m < HormoneNames.size(); m++)
        {
            if (HormoneNames.get(m).equals(HormoneName))
            {
                return HormoneStrengths.get(m);
            }
        }
        return Double.MAX_VALUE;
    }

    @Override
    protected void LoseExcitementAndAddExcitementForNextTick()
    {
        // Neuron-specific: in EACH simulation tick, set it to zero, because NeuronInputFields do always in EACH tick re-donate their COMPLETE flow!

        ExcitementCurrent = LimitExcitement(ExcitementCurrent);

        ShiftExcitementHistory(ExcitementCurrent);

        // Neuron-specific:
        double ForwardedExcitement = LimitExcitement(ExcitementCurrent);

        if (DoesActivate(ForwardedExcitement))
        {
            ((NeuronOutputNode) ExcitementPartner).ExciteAsPartner(ForwardedExcitement, this);
        }
        // end of specific

        // reset
        ExcitementCurrent = 0.0;
        ExcitementAddForNextTick = 0.0;
    }

    @Override
    public void Move(Point MoveAmount)
    {
        for (Iterator<NeuronInputField> i = InputFields.iterator(); i.hasNext();)
        {
            NeuronInputField o = i.next();
            o.Move(MoveAmount);
        }
        for (Iterator<NeuronInputNode> i = InputNodes.iterator(); i.hasNext();)
        {
            NeuronInputNode o = i.next();
            o.Move(MoveAmount);
        }
        for (Iterator<NeuronOutputNode> i = OutputNodes.iterator(); i.hasNext();)
        {
            NeuronOutputNode o = i.next();
            o.Move(MoveAmount);
        }

        Pos.x += MoveAmount.x;
        Pos.y += MoveAmount.y;
    }

    public void ReplaceHormoneName(String HormoneNameIn, String HormoneNameOut)
    {
        for (int i = 0; i < HormoneNames.size(); i++)
        {
            if (HormoneNames.get(i).equals(HormoneNameIn))
            {
                HormoneNames.set(i, HormoneNameOut);
            }
        }

        AutoSize();
    }

    @Override
    public void SetHighlighted(boolean IsHighlightedNew)
    {
        IsHighlighted = IsHighlightedNew;

        for (Iterator<NeuronInputField> i = InputFields.iterator(); i.hasNext();)
        {
            NeuronInputField o = i.next();
            o.SetHighlighted(IsHighlighted);
        }
        for (Iterator<NeuronInputNode> i = InputNodes.iterator(); i.hasNext();)
        {
            NeuronInputNode o = i.next();
            o.SetHighlighted(IsHighlighted);
        }
        for (Iterator<NeuronOutputNode> i = OutputNodes.iterator(); i.hasNext();)
        {
            NeuronOutputNode o = i.next();
            o.SetHighlighted(IsHighlighted);
        }
    }

    public void SetHormoneStrength(String ToUpdateName, double ToUpdateStrengthNew)
    { // just does nothing if HormoneName not found, you can safely call this method even in this case

        for (int m = 0; m < HormoneNames.size(); m++)
        {
            if (HormoneNames.get(m).equals(ToUpdateName))
            {
                HormoneStrengths.set(m, LimitHormoneStrength(ToUpdateStrengthNew));
            }
        }
    }

    @Override
    public void SetPartOfMultiSelection(boolean IsPartNew)
    {
        IsPartOfMultiSelection = IsPartNew;

        for (Iterator<NeuronInputField> i = InputFields.iterator(); i.hasNext();)
        {
            NeuronInputField o = i.next();
            o.SetPartOfMultiSelection(IsPartOfMultiSelection);
        }
        for (Iterator<NeuronInputNode> i = InputNodes.iterator(); i.hasNext();)
        {
            NeuronInputNode o = i.next();
            o.SetPartOfMultiSelection(IsPartOfMultiSelection);
        }
        for (Iterator<NeuronOutputNode> i = OutputNodes.iterator(); i.hasNext();)
        {
            NeuronOutputNode o = i.next();
            o.SetPartOfMultiSelection(IsPartOfMultiSelection);
        }
    }

    @Override
    public void SetPos(Point PosNew)
    {
        PosNew = AlignOnGrid(PosNew);

        Point MoveAmount = new Point(PosNew.x - Pos.x, PosNew.y - Pos.y);

        Move(MoveAmount);
    }

    public void Toggle(Point MousePos)
    {
        for (Iterator<NeuronInputField> i = InputFields.iterator(); i.hasNext();)
        {
            NeuronInputField o = i.next();
            if (o.ContainsPoint(MousePos))
                o.Toggle();
        }
    }

    public void ToggleInputFieldByIndex(int InputFieldIndex)
    {
        if (InputFieldIndex >= 0 && InputFieldIndex < InputFields.size())
        {
            if (InputFields.get(InputFieldIndex).Strength >= InputFields.get(InputFieldIndex).StrengthMax)
            {
                InputFields.get(InputFieldIndex).ToggleIsNegative();
            }
            InputFields.get(InputFieldIndex).Toggle();
        }
    }

    public void ToggleIsNegative(Point MousePos)
    {
        for (Iterator<NeuronInputField> i = InputFields.iterator(); i.hasNext();)
        {
            NeuronInputField o = i.next();
            if (o.ContainsPoint(MousePos))
            {
                o.ToggleIsNegative();
            }
        }
    }
}
